/*
 * CITS1002 Project 2 2012
 * Name:             Guilherme R. Lampert
 * Student number:   21203005
 * Date:             02/10/20012
 */

#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <ctype.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <utime.h>
#include "MergeDirs.h"

/*
 * Summary steps of the mergedirs program:
 *
 * parse command line arguments
 * recursively scan all input directories and get file lists
 *
 * for every input directory
 *    for every file in the directory
 *        test this file against all files from all the other input directories
 *        if this file should be copied, perform the copy
 *    end
 * end
 *
 * NOTES:
 * 1) Directories and filenames starting with a dot ('.') will always be ignored,
 * since these are usually hidden or system directories/files, under Unix and Linux.
 * 2) Empty directories and subdirectories will not be "merged", that is, if we have
 * an input dir named "foo" with an empty subdirectory "bar" inside it and an output "mydir",
 * "mydir" won't contain a subdirectory named "bar" after the merge.
 */

/* Local data */
static ProgramInput_t programInput;

#define CHECK_ALLOCATION(ptr)                                 \
    if (ptr == NULL) {                                        \
        Error("Memory allocation failed! Out of memory.\n");  \
    }

static void Error(const char * format, ...) {

	va_list args;

	if (format != NULL) {
		va_start(args, format);
		vfprintf(stderr, format, args);
		va_end(args);
	}

	exit(EXIT_FAILURE);
}

static void ParseCommandLine(int argc, char * argv[]) {

	int c;
	opterr = 0;

	while ((c = getopt(argc, argv, "lmvci:")) != -1) {
		switch (c) {
		case 'l' :
			programInput.l = 1;
			break;
		case 'm' :
			programInput.m = 1;
			break;
		case 'v' :
			programInput.v = 1;
			break;
		case 'c' :
			programInput.c = 1;
			break;
		case 'i' :
			if (programInput.ignorePattern != NULL) {
				Error("Only one -i option may be provided!\n");
			}
			programInput.i = 1;
			programInput.ignorePattern = strdup(optarg);
			CHECK_ALLOCATION(programInput.ignorePattern);
			break;
		case '?':
			if (optopt == 'i') {
				Error("Option -i requires an argument.\n");
			} else if (isprint(optopt)) {
				Error("Unknown option \'-%c\'.\n", optopt);
			} else {
				Error("Unknown option character \'\\x%x\'.\n", optopt);
			}
			break;
		default:
			Error("Failed to parse command line!\n");
		}
	}

	/* Will be 0 if none is specified, > 0 otherwise */
	int flagSum = programInput.c + programInput.l + programInput.m;

	if (flagSum == 0) {
		/* In the absence of any of the command-line options -c, -l, or -m,
		 * the default is to assume that (only) -m was provided. */
		programInput.m = 1;
		if (programInput.v) {
			printf("Default arg \'-m\' supplied...\n");
		}
	}

	if (flagSum > 1) {
		Error("Command line options \'-c\', \'-l\', and \'-m\', are all mutually exclusive!\n");
	}

	/* Non option arguments will be considered to be the
	 * input directories and the output directory */
	int argsLeft = argc - optind;

	if (argsLeft < 2) {
		Error("Must have at least 1 input directory and 1 output directory!\n");
	}

	programInput.outputDir = strdup(argv[argc - 1]);
	CHECK_ALLOCATION(programInput.outputDir);
	programInput.outputDirStrLen = strlen(programInput.outputDir);

	programInput.numInputDirs = argsLeft - 1;
	programInput.inputDirs = (DirEntry_t *)calloc(programInput.numInputDirs, sizeof(DirEntry_t));
	CHECK_ALLOCATION(programInput.inputDirs);

	int i = 0;
	int optIndex = optind;

	while (optIndex < (argc - 1)) {
		/* The input dirs */
		programInput.inputDirs[i].dirName = strdup(argv[optIndex]);
		CHECK_ALLOCATION(programInput.inputDirs[i].dirName);
		++optIndex;
		++i;
	}

	/* OK, now print options if on verbose mode */
	if (programInput.v) {
		printf("Options: ");
		if (programInput.l) { printf("-l "); }
		if (programInput.m) { printf("-m "); }
		if (programInput.v) { printf("-v "); }
		if (programInput.c) { printf("-c "); }
		if (programInput.i) { printf("-i="); }
		if (programInput.ignorePattern) {
			printf("%s\n", programInput.ignorePattern);
		} else {
			printf("\n");
		}
		printf("Input directories:\n");
		for (int i = 0; i < programInput.numInputDirs; ++i) {
			printf("%s\n", programInput.inputDirs[i].dirName);
		}
		printf("Output directory:\n");
		printf("%s\n", programInput.outputDir);
	}
}

static const char * SkipFirstDir(const char * path) {

	const char * p = path;
	while ((*p != '/') && (*p != '\\')) {
		++p;
	}
	return (++p);
}

static void CopyToOutputDir(const char * filename) {

	/* Tries to copy one file to the output directory. */
	assert(filename != NULL);

	/* +2 for the '\0' and possibly '/' in the end of outputDir */
	size_t newFileFullPathLen = programInput.outputDirStrLen + strlen(filename) + 2;
	char * newFileFullPath = (char *)malloc(newFileFullPathLen * sizeof(char));
	CHECK_ALLOCATION(newFileFullPath);

	strcpy(newFileFullPath, programInput.outputDir);
	if ((programInput.outputDir[programInput.outputDirStrLen - 1] != '/') &&
		(programInput.outputDir[programInput.outputDirStrLen - 1] != '\\')) {
		strcat(newFileFullPath, "/");
	}

	/* We need to get rid of the top level directory and replace it with the output directory */
	strcat(newFileFullPath, SkipFirstDir(filename));

	int result = CopyFile(filename, newFileFullPath);

	if (result != 0) {
		Error("Failed to copy \"%s\" to output dir \"%s\".\n"
		      "Reason: %s\n", filename, programInput.outputDir, strerror(result));
	}

	/* Set the copied file's access and last modification times
	 * to the same of the original file */
	struct stat statBuf;

	if (stat(filename, &statBuf) != 0) {
		Error("Failed to set \"%s\" access/modification times\n", newFileFullPath);
	}

	struct utimbuf utimeBuf;
	utimeBuf.actime = statBuf.st_atime;
	utimeBuf.modtime = statBuf.st_mtime;

	if (utime(newFileFullPath, &utimeBuf) != 0) {
		Error("Failed to set \"%s\" access/modification times\n", newFileFullPath);
	}

	/* All good :) */
	if (programInput.v) {
		printf("Copied \"%s\" to \"%s\"\n", filename, newFileFullPath);
	}
}

static int IsFilenameDirectory(const char * filename) {

	/* Is the given filename or path actually a directory? */
	assert(filename != NULL);

	struct stat statBuf;
	if (stat(filename, &statBuf) == 0) {
		return (S_ISDIR(statBuf.st_mode));
	} else {
		return (-1);
	}
}

static int ModificationTimeOfFile(const char * filename, time_t * mtime) {

	assert(filename != NULL && mtime != NULL);

	struct stat statBuf;
	if (stat(filename, &statBuf) == 0) {
		*mtime = statBuf.st_mtime;
		return (0);
	} else {
		return (-1);
	}
}

static void GetDirFilesRecursive(DirEntry_t * directory, const char * parentDir) {

	DIR * dirHandle;
	struct dirent * dirEntry;

	assert(directory != NULL && parentDir != NULL);

	if (programInput.v) {
		printf("Scaning: %s\n", parentDir);
	}

	dirHandle = opendir(parentDir);
	if (!dirHandle) {
		Error("Failed to open directory \"%s\"\n"
		      "Reason: %s\n", parentDir, strerror(errno));
	}

	unsigned int validFiles = 0;

	/* First pass is just to count the number of files/subdirectories */
	while ((dirEntry = readdir(dirHandle))) {
		/* Ignore "." , ".." and hidden files/dirs */
		if ((dirEntry->d_name[0] == '.') || (dirEntry->d_name[0] == '\0'))
			continue;
		++validFiles;
	}

	if (validFiles == 0) {
		/* "Empty" directory */
		closedir(dirHandle);
		return;
	}

	rewinddir(dirHandle);

	unsigned int nextFilenameIndex = directory->numFiles;

	/* Allocate new pointers for the filename strings */
	directory->numFiles += validFiles;
	directory->filenames = (char **)realloc(directory->filenames, directory->numFiles * sizeof(char *));
	CHECK_ALLOCATION(directory->filenames);

	/* OK, now read the actual filenames */
	while ((dirEntry = readdir(dirHandle))) {
		/* Ignore "." , ".." and hidden files/dirs */
		if ((dirEntry->d_name[0] == '.') || (dirEntry->d_name[0] == '\0'))
			continue;

		/* +2 for the '\0' and possibly '/' in the end of parentDirLen */
		size_t parentDirLen = strlen(parentDir);
		size_t fullNameLen = parentDirLen + strlen(dirEntry->d_name) + 2;
		char * fullName = (char *)malloc(fullNameLen * sizeof(char));
		CHECK_ALLOCATION(fullName);

		strcpy(fullName, parentDir);
		if ((parentDir[parentDirLen - 1] != '/') &&
			(parentDir[parentDirLen - 1] != '\\')) {
			strcat(fullName, "/");
		}
		strcat(fullName, dirEntry->d_name);

		/* Right, now is time to check if we have subdirectories here and recurse if so */
		int isDir = IsFilenameDirectory(fullName);
		if (isDir == -1) {
			/* Error, impossible to get directory info, abort */
			Error("Failed to get information about directory/file \"%s\"\n"
			      "Reason: %s\n", fullName, strerror(errno));
		}
		if (isDir) {
			/* It is actually a subdirectory, scan it, if not matching the ignore pattern */
			int ignoreSubdir = 0;
			if (programInput.i) {
				if (strstr(fullName, programInput.ignorePattern)) {
					ignoreSubdir = 1;
				}
			}

			if (!ignoreSubdir) {
				directory->filenames[nextFilenameIndex++] = NULL;
				GetDirFilesRecursive(directory, fullName);
			}

			free(fullName);
		} else {
			/* Only files are added to the 'filenames' array */
			directory->filenames[nextFilenameIndex++] = fullName;
		}
	}

	closedir(dirHandle);
}

static void ScanInputDirs(void) {

	if (programInput.v) {
		printf("Scanning input directories...\n");
	}

	char bigBuf[2048] = {0};
	size_t totalFilesToTryMerge = 0;

	for (size_t i = 0; i < programInput.numInputDirs; ++i) {
		/* Open all input directories and its subdirectories and retrieve its files */
		const char * inDir = programInput.inputDirs[i].dirName;
		size_t inDirLen = strlen(inDir);
		strcpy(bigBuf, inDir);
		if ((bigBuf[inDirLen - 1] != '/') &&
			(bigBuf[inDirLen - 1] != '\\')) {
			strcat(bigBuf, "/");
		}

		GetDirFilesRecursive(&programInput.inputDirs[i], bigBuf);

		/* Output of GetDirFilesRecursive() will need some processing,
		 because the 'filenames' array mays contain null pointers in it,
		 due to the remotion of ignored empty subdirs and ignored by pattern subdirs. */
		char ** filenames = programInput.inputDirs[i].filenames;
		size_t numFiles = programInput.inputDirs[i].numFiles;
		size_t j = 0;

		while (j < numFiles) {
			if (filenames[j] == NULL) {
				for (size_t k = j; k < numFiles; ++k) {
					filenames[k] = filenames[k + 1];
				}
				--numFiles;
			} else {
				++j;
			}
		}
		programInput.inputDirs[i].numFiles = numFiles;
		totalFilesToTryMerge += numFiles;
	}

	if (programInput.v) {
		printf("Total files found: %zu\n", totalFilesToTryMerge);
	}
}

static int ShouldCopyFile(const DirEntry_t * currentDirectory, const char * currentFilename) {

	const char * currFile = SkipFirstDir(currentFilename);
	int shouldCopy = 0;

	for (size_t idir = 0; idir < programInput.numInputDirs; ++idir) {
		DirEntry_t * directory = &programInput.inputDirs[idir];
		if (directory != currentDirectory) {
			for (size_t ifile = 0; ifile < directory->numFiles; ++ifile) {
				const char * aFile = SkipFirstDir(directory->filenames[ifile]);
				if (strcmp(aFile, currFile) == 0) {
					/* File names are the same, must check other criteria to break the tie */
					if (programInput.l) {
						/* Largest of the 2 should be copied */
						off_t currFileLen, aFileLen;
						if (LengthOfFile(currentFilename, &currFileLen) != 0) {
							Error("Failed to get size of file \"%s\"\n", currentFilename);
						}
						if (LengthOfFile(directory->filenames[ifile], &aFileLen) != 0) {
							Error("Failed to get size of file \"%s\"\n", directory->filenames[ifile]);
						}
						if (currFileLen == aFileLen) {
							Error("Conflict! \"%s\" and \"%s\"\n"
							      "have the same size and flag -l is specified\n",
							      currentFilename, directory->filenames[ifile]);
						}
						shouldCopy = (currFileLen > aFileLen) ? 1 : 0;
					} else if (programInput.m) {
						/* File with the most-recent modification-time should be copied */
						time_t currFileMtime, aFileMtime;
						if (ModificationTimeOfFile(currentFilename, &currFileMtime) != 0) {
							Error("Failed to get mtime of file \"%s\"\n", currentFilename);
						}
						if (ModificationTimeOfFile(directory->filenames[ifile], &aFileMtime) != 0) {
							Error("Failed to get mtime of file \"%s\"\n", directory->filenames[ifile]);
						}
						if (currFileMtime == aFileMtime) {
							Error("Conflict! \"%s\" and \"%s\"\n"
							      "have the same modification time and flag -m is specified\n",
							      currentFilename, directory->filenames[ifile]);
						}
						shouldCopy = (currFileMtime > aFileMtime) ? 1 : 0;
					} else if (programInput.c) {
						/* Check file contents first, if they are equal, doesn't
						 matter, copy any of 'em. If the contents differ, we have a
						 insolvable conflict, so just report it and terminate */
						char currFileHash[68], aFileHash[68];
						if (HashFile(currentFilename, currFileHash) != 0) {
							Error("Failed to compute checksum for file \"%s\"\n", currentFilename);
						}
						if (HashFile(directory->filenames[ifile], aFileHash) != 0) {
							Error("Failed to compute checksum for file \"%s\"\n", directory->filenames[ifile]);
						}
						if (strcmp(currFileHash, aFileHash) == 0) {
							/* Contents are the same, should copy */
							shouldCopy = 1;
						} else {
							/* They differ, that is a conflict! */
							Error("%s and %s are in conflict!\n"
							      "File contents are the same in both, stopping merge...\n",
							      currentFilename, directory->filenames[ifile]);
						}
					} else {
						/* This should never happen! At least one flag must be defined */
						abort();
					}
				} else {
					/* Different names, can safely copy */
					shouldCopy = 1;
				}
			}
		}
	}

	return (shouldCopy);
}

static void MergeDirs(void) {

	if (programInput.v) {
		printf("Merging directories...\n");
	}

	DirEntry_t * directory;

	if (programInput.numInputDirs == 1) {
		/* Special case, only one input dir, just clone it to the output */
		directory = &programInput.inputDirs[0];
		for (size_t i = 0; i < directory->numFiles; ++i) {
			CopyToOutputDir(directory->filenames[i]);
		}
	} else {
		for (size_t idir = 0; idir < programInput.numInputDirs; ++idir) {
			directory = &programInput.inputDirs[idir];
			for (size_t ifile = 0; ifile < directory->numFiles; ++ifile) {
				const char * filename = directory->filenames[ifile];
				if (ShouldCopyFile(directory, filename)) {
					CopyToOutputDir(filename);
				}
			}
		}
	}

	if (programInput.v) {
		printf("Done!\n");
	}
}

int main(int argc, char * argv[]) {

	ParseCommandLine(argc, argv);

	ScanInputDirs();

	MergeDirs();

	return (0);
}
